using System;
using System.Collections.Generic;
using System.Text;
using System.Net.Sockets;
using System.Xml;
using System.Net.Security;
using System.Security.Authentication;
using System.IO;
using System.Reflection;
using System.Net;
using LibGreen.Jabber.Delegates;
using LibGreen.Jabber.Dispatchers;
using LibGreen.Jabber.ProtocolElements;
using LibGreen.Jabber.ProtocolElements.IqElements;
using LibGreen.Jabber.Utility;



namespace LibGreen.Jabber.Connection
{
    /// <summary>
    /// Describes the state of an <see cref="XmppConnection"/>
    /// </summary>
    public enum ConnectionState
    {
        /// <summary>
        /// Not connected to the server
        /// </summary>
        Disconnected,
        /// <summary>
        /// Connecting to the server
        /// </summary>
        Connecting,
        /// <summary>
        /// Negotiating capabilities
        /// </summary>
        Negotiating,
        /// <summary>
        /// Securing the connection using TLS
        /// </summary>
        Securing,
        /// <summary>
        /// Authenticating the connection using SASL
        /// </summary>
        Authenticating,
        /// <summary>
        /// Binding to a server resource
        /// </summary>
        Binding,
        /// <summary>
        /// Connected to the server
        /// </summary>
        Connected,
    }

    /// <summary>
    /// A single connection to an XMPP source
    /// </summary>
    public class XmppConnection
    {
        /// <summary>
        /// The current state of the connection
        /// </summary>
        protected ConnectionState connectionState = ConnectionState.Disconnected;
        /// <summary>
        /// The network stream used to initialize the connection to the server
        /// </summary>
        protected TcpClient tcpClient;
        /// <summary>
        /// The stream underlying the connection; either a NetworkStream or SslStream
        /// </summary>
        protected Stream baseStream;
        /// <summary>
        /// An XmlWriter used to send packets to the server
        /// </summary>
        protected XmlWriter xmlWriter;
        /// <summary>
        /// An XmlReader used to read packets from the server
        /// </summary>
        protected AsyncXmlReader xmlReader;

        /// <summary>
        /// The &lt;stream:features&gt; element sent on the initial stream
        /// </summary>
        private FeaturesElement initialStreamFeatures;
        /// <summary>
        /// The &lt;stream:features&gt; element sent on the TLS/SASL stream
        /// </summary>
        private FeaturesElement authStreamFeatures;
        /// <summary>
        /// The &lt;stream:features&gt; element sent on the authenticated stream
        /// </summary>
        private FeaturesElement liveStreamFeatures;

        /// <summary>
        /// A list of dispatchers registered to this connection
        /// </summary>
        private List<IDispatcher> dispatchers = new List<IDispatcher>();

        private SASLNegotiator negotiator;
        private readonly string server;
        private readonly int port;
        private readonly Session parent;

        /// <summary>
        /// Raised when the connection fails
        /// </summary>
        public event ConnectionFailedHandler ConnectionFailed;
        /// <summary>
        /// Raised when the connection status is updated
        /// </summary>
        public event ConnectionUpdatedHandler ConnectionUpdated;
        /// <summary>
        /// Raised when the connection is completed
        /// </summary>
        public event ConnectionFinishedHandler ConnectionFinished;

        /// <summary>
        /// Initializes a new XmppConnection
        /// </summary>
        /// <param name="parent">The <see cref="Session"/> that owns this connection</param>
        /// <param name="server">The XMPP server to which to connect</param>
        /// <param name="port">The port to which to connect</param>
        public XmppConnection(Session parent, string server, int port)
        {
            this.parent = parent;
            this.server = server;
            this.port = port;
        }

        #region Public methods
        /// <summary>
        /// Begins the connection to the server
        /// </summary>
        public void ConnectToServer()
        {
            if (baseStream != null || State != ConnectionState.Disconnected)
            {
                Disconnect();
            }

            parent.ProxiedSocketFactory(server, port, new ProxiedSocketFactoryResultHandler(ProxiedSocketFactoryResult));

            //tcpClient = new TcpClient();
            //tcpClient.BeginConnect(server, port, EndConnect, null);
            ChangeConnectionState(ConnectionState.Connecting);
        }

        /// <summary>
        /// Disconnects from the server
        /// </summary>
        public void Disconnect()
        {
            ChangeConnectionState(ConnectionState.Disconnected);
            if (baseStream != null)
            {
                if (xmlReader != null)
                {
                    xmlReader.Close();
                    xmlReader = null;
                }
                if (xmlWriter != null)
                {
                    xmlWriter.Close();
                    xmlWriter = null;
                }

                baseStream.Close();
                baseStream.Dispose();
                baseStream = null;
            }
        }

        /// <summary>
        /// Sends an XMPP element to the server
        /// </summary>
        /// <param name="element">The <see cref="Element"/> to send</param>
        public void SendXmppElement(Element element)
        {
            if (connectionState == ConnectionState.Disconnected || xmlWriter == null)
            {
                // TODO:  Throw exception here
            }

            string elementStr = element.ToString();

            xmlWriter.WriteRaw(elementStr);
            xmlWriter.Flush();
        }

        /// <summary>
        /// Add a custom command dispatcher to this connection
        /// </summary>
        /// <param name="dispatcher">An <see cref="IDispatcher"/> to add to the connection's command processor</param>
        public void AddDispatcher(IDispatcher dispatcher)
        {
            lock (dispatchers)
            {
                if (!dispatchers.Contains(dispatcher))
                {
                    dispatchers.Add(dispatcher);
                    dispatcher.Initialize();
                }
            }
        }

        /// <summary>
        /// Remove a custom command dispatcher from this connection
        /// </summary>
        /// <param name="dispatcher">An <see cref="IDispatcher"/> to remove from the connection's command processor</param>
        public void RemoveDispatcher(IDispatcher dispatcher)
        {
            lock (dispatchers)
            {
                if (dispatchers.Contains(dispatcher))
                {
                    dispatchers.Remove(dispatcher);
                }
            }
        }
        #endregion

        #region Public properties
        /// <summary>
        /// Gets the server to which this <see cref="XmppConnection"/> is connected
        /// </summary>
        public string Server
        {
            get { return server; }
        }

        /// <summary>
        /// Gets the port to which this <see cref="XmppConnection"/> is connected
        /// </summary>
        public int Port
        {
            get { return port; }
        }

        /// <summary>
        /// Gets the current <see cref="ConnectionState"/> of the connection
        /// </summary>
        public ConnectionState State
        {
            get { return connectionState; }
        }
        #endregion

        #region Protected methods
        /// <summary>
        /// Handles the result of the proxy socket factory connection attempt
        /// </summary>
        void ProxiedSocketFactoryResult(Socket sock, string errormsg)
        {
            if (!String.IsNullOrEmpty(errormsg))
            {
                if (connectionState != ConnectionState.Disconnected)
                {
                    Logging.LogString("Could not connect socket: {0}", errormsg);
                    OnConnectionFailed(ConnectionFailureReason.SocketFailure);
                    Disconnect();
                }
                return;
            }

            ChangeConnectionState(ConnectionState.Negotiating);
            baseStream = new NetworkStream(sock, true);
            InitializeReaderWriter();

            // Start the read loop
            xmlReader.ReadAsync();

            // Send the stream:stream beginning element
            SendXmppElement(StreamElement.Create(parent.JID.Domain));
        }

        /// <summary>
        /// Initializes the XML reader and writer around the currently active network stream
        /// </summary>
        private void InitializeReaderWriter()
        {
            if (xmlReader != null)
            {
                xmlReader.Close();
            }
            if (xmlWriter != null)
            {
                xmlWriter.Close();
            }

            // Create the XML reader and writer for the connection
            XmlReaderSettings readerSettings = new XmlReaderSettings();
            readerSettings.CloseInput = false;
            readerSettings.IgnoreProcessingInstructions = true;
            readerSettings.IgnoreWhitespace = true;
            xmlReader = new AsyncXmlReader(baseStream, readerSettings);
            xmlReader.ReadComplete += new ReadCompleteHandler(xmlReader_ReadComplete);

            XmlWriterSettings writerSettings = new XmlWriterSettings();
            writerSettings.CloseOutput = false;
            writerSettings.OmitXmlDeclaration = false;
            xmlWriter = XmlWriter.Create(baseStream);
        }

        /// <summary>
        /// Handles the completion of an asynchronous XMPP element read
        /// </summary>
        /// <param name="sender">The <see cref="AsyncXmlReader"/> that raised the event</param>
        /// <param name="e">The status of the completed read operation</param>
        private void xmlReader_ReadComplete(object sender, ReadCompleteEventArgs e)
        {
            switch (connectionState)
            {
                case ConnectionState.Negotiating:
                    StreamElement firstStream = new StreamElement(new Element(xmlReader.Base));
                    Element firstFeatures = new Element(xmlReader.Base);
                    if (firstFeatures.Name == "stream:features")
                    {
                        initialStreamFeatures = new FeaturesElement(firstFeatures);

                        // Shift to the next connection state and tell the server we're ready to secure
                        ChangeConnectionState(ConnectionState.Securing);
                        SendXmppElement(StartTlsElement.Create());
                    }
                    else
                    {
                        // TODO:  Error?  Can this happen?
                    }
                    break;

                case ConnectionState.Securing:
                    Element tlsResponse = new Element(xmlReader.Base);
                    if (tlsResponse.Name == "proceed")
                    {
                        // Create a TlsInitializer handler
                        TlsInitializer initializer = new TlsInitializer(server, baseStream);
                        initializer.TlsFailure += delegate
                        {
                            OnConnectionFailed(ConnectionFailureReason.TLSFailure);
                            Disconnect();
                        };
                        initializer.TlsInitialized += delegate
                        {
                            baseStream = initializer.SecuredStream;

                            // Reinitialize the reader and writer on the secured stream
                            InitializeReaderWriter();

                            // Shift to authentication mode and start a new <stream:stream> with the server
                            ChangeConnectionState(ConnectionState.Authenticating);
                            // Restart the read loop
                            xmlReader.ReadAsync();
                            // Start the new stream:stream
                            SendXmppElement(StreamElement.Create(parent.JID.Domain));
                        };
                        initializer.InitializeTls();
                        return; // We don't want to enter the read loop during SSL negotiation
                    }
                    else if (tlsResponse.Name == "failure")
                    {
                        OnConnectionFailed(ConnectionFailureReason.TLSFailure);
                        Disconnect();
                    }
                    break;


                case ConnectionState.Authenticating:
                    if (negotiator == null)
                    {
                        StreamElement secondStream = new StreamElement(new Element(xmlReader.Base));
                        Element secondFeatures = new Element(xmlReader.Base);
                        if (secondFeatures.Name == "stream:features")
                        {
                            authStreamFeatures = new FeaturesElement(secondFeatures);

                            negotiator = new SASLNegotiator(this);
                            negotiator.NegotiationFailed += SaslNegotiationFailed;
                            negotiator.NegotiationSucceeded += SaslNegotiationSucceeded;
                            negotiator.StartNegotiation(authStreamFeatures);
                        }
                        else
                        {
                            // TODO:  Error?
                        }
                    }
                    else
                    {
                        // If we're in an Authenticating state and the negotiator was already
                        // created by the arrival of a new stream, the negotiator's responsible
                        // for handling all received elements. The negotiator will cause a
                        // connection state shift when it has either succeeded or failed.
                        negotiator.DataReceived(new Element(xmlReader.Base));
                    }
                    break;

                case ConnectionState.Binding:
                    {
                        Element element = new Element(xmlReader.Base);
                        if (element.Name == "stream:stream")
                        {
                            // Initialization of the new stream post-SASL negotiation
                            //StreamElement bindingStream = new StreamElement(element);
                            Element thirdFeatures = new Element(xmlReader.Base);
                            if (thirdFeatures.Name == "stream:features")
                            {
                                liveStreamFeatures = new FeaturesElement(thirdFeatures);
                                if (liveStreamFeatures.IsBindingSupported)
                                {
                                    SendXmppElement(IqBindElement.Create(parent.JID.Resource));
                                }
                                else
                                {
                                    ChangeConnectionState(ConnectionState.Connected);
                                    OnConnectionFinished();
                                }
                            }
                            else
                            {
                                //TODO:  Error?
                            }
                        }
                        else if (element.Name == "iq")
                        {
                            IqElement iqElement = new IqElement(element);
                            if (iqElement.Type == IqType.Result)
                            {
                                // Get the <bind> result
                                foreach (Element iqChild in iqElement.Children)
                                {
                                    if (iqChild.Name == "bind")
                                    {
                                        // <jid> is the 0th child, the bound JID is its value
                                        string jid = iqChild.Children[0].Value;
                                        parent.JID.Resource = Jid.GetJidSection(jid, JidPart.Resource);
                                        break;
                                    }
                                }

                                ChangeConnectionState(ConnectionState.Connected);
                                OnConnectionFinished();
                                AddDispatcher(new SessionDispatcher(this));
                            }
                            else if (iqElement.Type == IqType.Error)
                            {
                                string errorReason = "";
                                foreach (Element iqChild in iqElement.Children)
                                {
                                    if (iqChild.Name == "error")
                                    {
                                        // The 0th child is an element whose name is the error reason
                                        errorReason = iqChild.Children[0].Name;
                                        break;
                                    }
                                }

                                OnConnectionFailed(ConnectionFailureReason.BindingFailure);
                            }
                        }
                    }
                    break;

                case ConnectionState.Connected:
                    DispatchElement(new Element(xmlReader.Base));
                    break;
            }

            // Reset the read loop unless the connection has been disconnected
            if (connectionState != ConnectionState.Disconnected)
            {
                xmlReader.ReadAsync();
            }
        }

        #region SASLNegotiator events
        /// <summary>
        /// Moves the connection state to <see cref="ConnectionState.Binding"/> and starts a new XML stream
        /// </summary>
        /// <param name="sender">The <see cref="SASLNegotiator"/> raising the event</param>
        /// <param name="e">Not used</param>
        protected internal void SaslNegotiationSucceeded(object sender, EventArgs e)
        {
            DisposeSaslNegotiator();

            // Reinitialize the reader and writer
            InitializeReaderWriter();

            ChangeConnectionState(ConnectionState.Binding);
            SendXmppElement(StreamElement.Create(parent.JID.Domain));
        }

        /// <summary>
        /// Raises the <see cref="ConnectionFailed"/> event when SASL negotiation fails
        /// </summary>
        /// <param name="sender">The <see cref="SASLNegotiator"/> raising the event</param>
        /// <param name="e">A description of the failure</param>
        protected internal void SaslNegotiationFailed(object sender, SaslNegotiationFailedEventArgs e)
        {
            DisposeSaslNegotiator();
            OnConnectionFailed(ConnectionFailureReason.SASLFailure);
            Disconnect();
        }

        /// <summary>
        /// Releases the SASLNegotiator resources
        /// </summary>
        private void DisposeSaslNegotiator()
        {
            if (negotiator != null)
            {
                negotiator.NegotiationFailed -= SaslNegotiationFailed;
                negotiator.NegotiationSucceeded -= SaslNegotiationSucceeded;
                negotiator = null;
            }
        }
        #endregion

        #region Event raisers
        /// <summary>
        /// Raises the <see cref="ConnectionFinished"/> event
        /// </summary>
        protected internal void OnConnectionFinished()
        {
            if (ConnectionFinished != null)
            {
                ConnectionFinished(this, new EventArgs());
            }
        }

        /// <summary>
        /// Raises the <see cref="ConnectionFailed"/> event
        /// </summary>
        /// <param name="reason">The reason for the connection failure</param>
        protected internal void OnConnectionFailed(ConnectionFailureReason reason)
        {
            if (ConnectionFailed != null)
            {
                ConnectionFailed(this, new ConnectionFailedEventArgs(reason));
            }
        }

        /// <summary>
        /// Moves the connection process to a new state and raises the <see cref="ConnectionUpdated"/> event
        /// </summary>
        /// <param name="newState">The new connection state</param>
        protected internal void ChangeConnectionState(ConnectionState newState)
        {
            ConnectionState oldState = connectionState;
            connectionState = newState;
            if (ConnectionUpdated != null)
            {
                ConnectionUpdated(this, new ConnectionUpdatedEventArgs(oldState, newState));
            }
        }
        #endregion

        /// <summary>
        /// Dispatch a received element
        /// </summary>
        private void DispatchElement(Element element)
        {
            lock (dispatchers)
            {
                foreach (IDispatcher dispatcher in dispatchers)
                {
                    if (dispatcher.DispatchElement(element))
                    {
                        return;
                    }
                }
            }

            Logging.LogString("Unhandled element: " + element);
        }

        /// <summary>
        /// Gets the session that owns this connection
        /// </summary>
        internal Session Parent
        {
            get { return parent; }
        }

        /// <summary>
        /// Gets the <see cref="FeaturesElement"/> for the authenticated and established &lt;stream:stream&gt;
        /// </summary>
        internal FeaturesElement StreamFeatures
        {
            get { return liveStreamFeatures; }
        }
        #endregion

        #region TLS initializer
        /// <summary>
        /// A utility class to authenticate a <see cref="TcpClient"/>'s stream as SSL using the TLS protocol
        /// </summary>
        private class TlsInitializer
        {
            /// <summary>
            /// Raised when TLS initialization is complete
            /// </summary>
            public event EventHandler TlsInitialized;
            /// <summary>
            /// Raised when TLS could not be initialized with the remote host
            /// </summary>
            public event EventHandler TlsFailure;

            private SslStream newStream;
            private readonly string server;
            private readonly Stream clientStream;

            /// <summary>
            /// Creates a new TlsInitializer for a <see cref="TcpClient"/>
            /// </summary>
            public TlsInitializer(string server, Stream clientStream)
            {
                this.server = server;
                this.clientStream = clientStream;
            }

            /// <summary>
            /// Begins the process of TLS initialization
            /// </summary>
            public void InitializeTls()
            {
                // Create an SSL stream that always accepts remote certificates as valid
                newStream = new SslStream(clientStream, true, delegate { return true; });

                try
                {
                    newStream.BeginAuthenticateAsClient(server, null, SslProtocols.Tls, true,
                        EndAuthenticate, null);
                }
                catch (AuthenticationException authex)
                {
                    Logging.LogString("Caught AuthenticationException in XmppConnection.InitializeTls: {0}", authex);
                    OnTlsFailure();
                }
            }

            /// <summary>
            /// Gets the connected <see cref="SslStream"/>
            /// </summary>
            public SslStream SecuredStream
            {
                get { return newStream; }
            }

            /// <summary>
            /// Ends the SSL stream authentication
            /// </summary>
            /// <param name="res">The <see cref="IAsyncResult"/></param>
            private void EndAuthenticate(IAsyncResult res)
            {
                try
                {
                    newStream.EndAuthenticateAsClient(res);
                    OnTlsInitialized();
                }
                catch (Exception ex)
                {
                    Logging.LogString("Caught exception in TlsInitializer.EndAuthenticate: {0}", ex);
                    OnTlsFailure();
                }
            }

            /// <summary>
            /// Raises the <see cref="TlsFailure"/> event
            /// </summary>
            private void OnTlsFailure()
            {
                if (TlsFailure != null)
                {
                    TlsFailure(this, new EventArgs());
                }
            }

            /// <summary>
            /// Raises the <see cref="TlsInitialized"/> event
            /// </summary>
            private void OnTlsInitialized()
            {
                if (TlsInitialized != null)
                {
                    TlsInitialized(this, new EventArgs());
                }
            }
        }
        #endregion

        #region Socket factory support
        /// <summary>
        /// Implements an asynchronous socket connection factory that operates with no network proxy
        /// </summary>
        /// <param name="host">The host to which to connect</param>
        /// <param name="port">The port on the host to which to connect</param>
        /// <param name="callback">A <see cref="Delegate"/> to be called when the socket creation is complete</param>
        /// <remarks>This is a public wrapper to the internal method <see cref="Connection.CreateDirectConnectSocket"/></remarks>
        public static void DirectSocketConnectionFactory(string host, int port, Delegate callback)
        {
            CreateDirectConnectSocket(host, port, callback);
        }

        /// <summary>
        /// Encapsulates socket connection information
        /// </summary>
        private class SocketConnectionData
        {
            public Delegate Callback;
            public int Port;
            public string Server;
            public Socket Socket;
        }

        /// <summary>
        /// Begins the socket creation process
        /// </summary>
        public static void CreateDirectConnectSocket(string host, int port, Delegate callback)
        {
            // Make sure the callback is the right type
            // TODO: Better error prevention for CF?
#if !WindowsCE
            ParameterInfo[] param = callback.Method.GetParameters();
            if (param.Length != 2 || param[0].ParameterType != typeof(Socket) ||
                param[1].ParameterType != typeof(string))
            {
                throw new ArgumentException("Callback delegate must take a Socket and a string as its parameters");
            }
#endif

            SocketConnectionData scd = new SocketConnectionData();
            scd.Callback = callback;
            scd.Port = port;
            scd.Server = host;
            scd.Socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

            try
            {
                Dns.BeginGetHostEntry(scd.Server, new AsyncCallback(CreateDCSEndDnsLookup), scd);
            }
            catch (SocketException sockex)
            {
                string message = "Cannot connect to DNS service: " + sockex.Message;

#if WindowsCE
		ProxiedSocketFactoryResultHandler handler = (callback as ProxiedSocketFactoryResultHandler);
		handler(scd.Socket, message);
#else
                callback.DynamicInvoke(scd.Socket, message);
#endif
            }
        }

        /// <summary>
        /// Ends the DNS lookup phase of connection and begins connecting the socket to the host
        /// </summary>
        protected static void CreateDCSEndDnsLookup(IAsyncResult res)
        {
            SocketConnectionData scd = null;

            IPHostEntry hosts = null;
            try
            {
                scd = res.AsyncState as SocketConnectionData;
                hosts = Dns.EndGetHostEntry(res);
            }
            catch (Exception sockex)
            {
                string message = "Cannot resolve server: " + sockex.Message;

#if WindowsCE
		ProxiedSocketFactoryResultHandler handler = (scd.Callback as ProxiedSocketFactoryResultHandler);
		handler(scd.Socket, message);
#else
                scd.Callback.DynamicInvoke(scd.Socket, message);
#endif
                return;
            }

            IPAddress address = hosts.AddressList[0];
            IPEndPoint ipep = new IPEndPoint(address, scd.Port);

            try
            {
                scd.Socket.BeginConnect(ipep, new AsyncCallback(CreateDCSEndInitialConnection), scd);
            }
            catch (SocketException sockex)
            {
                string message = "Cannot connect to server: " + sockex.Message;

#if WindowsCE
		ProxiedSocketFactoryResultHandler handler = (scd.Callback as ProxiedSocketFactoryResultHandler);
		handler(scd.Socket, message);
#else
                scd.Callback.DynamicInvoke(scd.Socket, message);
#endif
            }
        }

        /// <summary>
        /// Ends the connection phase and returns a connected socket
        /// </summary>
        protected static void CreateDCSEndInitialConnection(IAsyncResult res)
        {
            SocketConnectionData scd = null;
            try
            {
                scd = res.AsyncState as SocketConnectionData;
                scd.Socket.EndConnect(res);
#if WindowsCE
		ProxiedSocketFactoryResultHandler handler = (scd.Callback as ProxiedSocketFactoryResultHandler);
		handler(scd.Socket, "");
#else
                scd.Callback.DynamicInvoke(scd.Socket, "");
#endif
            }
            catch (Exception sockex)
            {
                string message = "Can't connect to server: " + sockex.Message;

#if WindowsCE
		ProxiedSocketFactoryResultHandler handler = (scd.Callback as ProxiedSocketFactoryResultHandler);
		handler(scd.Socket, message);
#else
                scd.Callback.DynamicInvoke(scd.Socket, message);
#endif
            }
        }

        #endregion
    }
}
